/*====================================================================== 
 *         A globalfifo driver as an example of char device drivers   
 *         The initial developer of the original code is Baohua Song 
 *         <author@linuxdriver.cn>. All Rights Reserved. 
 *         modify by wangxinyong 2011.11
 * ======================================================================*/  

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/poll.h>
#include <linux/semaphore.h>
#include <linux/wait.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
//#include <asm/atomic.h>
#include <asm/fcntl.h>
//#include <asm/poll.h>


#define GLOBALFIFO_SIZE 	0x1000
#define MEM_CLEAR 	0x1
#define GLOBALFIFO_MAJOR 0

#define GLOBALFIFO_IOC_MAGIC 'w'
#define GLOBALFIFO_IOCCLEAR  _IO(GLOBALFIFO_IOC_MAGIC, 0)

static int globalfifo_major = GLOBALFIFO_MAJOR;

struct globalfifo_dev {
	struct cdev cdev;
	unsigned int current_len;
	unsigned char mem[GLOBALFIFO_SIZE];
	struct semaphore globalfifo_sem;
	wait_queue_head_t read_wait;
	wait_queue_head_t write_wait;
};

struct globalfifo_dev *globalfifo_devp;
struct class *globalfifo_class;
//static atomic_t globalfifo_available = ATOMIC_INIT(1);

int globalfifo_open(struct inode *inode, struct file *filp)
{
/*	if (!atomic_dec_and_test(&globalfifo_available)) {
		atomic_inc(&globalfifo_available);
		return -EBUSY;
	}
*/
	filp->private_data = globalfifo_devp;
	return 0;
}

int globalfifo_release(struct inode *inode, struct file *filp)
{
	
//	atomic_inc(&globalfifo_available);
	return 0;
}

static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
	ssize_t retval;

	struct globalfifo_dev *dev = filp->private_data;

	DECLARE_WAITQUEUE(wait, current);

	if (down_interruptible(&dev->globalfifo_sem)) { //not available return
		return -ERESTARTSYS;
	}
	
	add_wait_queue(&dev->read_wait,&wait);

	if (dev->current_len == 0) {
		if (filp->f_flags & O_NONBLOCK) {
			retval = -EAGAIN;
			up(&dev->globalfifo_sem);
			remove_wait_queue(&dev->write_wait, &wait);
                        set_current_state(TASK_RUNNING);
			return retval;
		}
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&dev->globalfifo_sem);

		schedule();
		if (signal_pending(current)) {
			retval = -ERESTARTSYS;
			remove_wait_queue(&dev->write_wait, &wait);
			set_current_state(TASK_RUNNING);
			return retval;
		}
		
		if (down_interruptible(&dev->globalfifo_sem)) { //not available return
			return -ERESTARTSYS;
		}
	}
	
	if (count > dev->current_len) {
		count = dev->current_len;
	}
	
	if (copy_to_user(buf, dev->mem, count)) {
		retval = -EFAULT;
		up(&dev->globalfifo_sem);
                remove_wait_queue(&dev->write_wait, &wait);
                set_current_state(TASK_RUNNING);
                return retval;
	}
	else {
		memcpy(dev->mem, dev->mem + count, dev->current_len - count);
		dev->current_len -= count;
		printk(KERN_INFO "read %d bytes, current_len=%d\n",count ,dev->current_len);
		wake_up_interruptible(&dev->write_wait);
		retval = count;
		up(&dev->globalfifo_sem);
                remove_wait_queue(&dev->write_wait, &wait);
                set_current_state(TASK_RUNNING);
                return retval;
	}
}

static ssize_t globalfifo_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
	size_t retval;
	struct globalfifo_dev *dev = filp->private_data;

	DECLARE_WAITQUEUE(wait, current);
	if (down_interruptible(&dev->globalfifo_sem)) { //not available return
        	return -ERESTARTSYS;
        }

        add_wait_queue(&dev->write_wait,&wait);
	
	if (dev->current_len == GLOBALFIFO_SIZE) {
                if (filp->f_flags & O_NONBLOCK) {
                        retval = -EAGAIN;
                        up(&dev->globalfifo_sem);
                        remove_wait_queue(&dev->write_wait, &wait);
                        set_current_state(TASK_RUNNING);
                        return retval;
                }
                __set_current_state(TASK_INTERRUPTIBLE);
                up(&dev->globalfifo_sem);

                schedule();
                if (signal_pending(current)) {
                        retval = -ERESTARTSYS;
                        remove_wait_queue(&dev->write_wait, &wait);
                        set_current_state(TASK_RUNNING);
                        return retval;
                }

                if (down_interruptible(&dev->globalfifo_sem)) { //not available return
                        return -ERESTARTSYS;
                }
        }

	if (count > GLOBALFIFO_SIZE - dev->current_len) 
		count = GLOBALFIFO_SIZE - dev->current_len;
	if (copy_from_user(dev->mem + dev->current_len, buf, count)) {
		retval = -EFAULT;
		up(&dev->globalfifo_sem);
                remove_wait_queue(&dev->write_wait, &wait);
                set_current_state(TASK_RUNNING);
		return retval;
	}
	else {
		dev->current_len += count;
                printk(KERN_INFO "write %d bytes, current_len=%d\n",count ,dev->current_len);
                wake_up_interruptible(&dev->read_wait);
                retval = count;
		up(&dev->globalfifo_sem);
                remove_wait_queue(&dev->write_wait, &wait);
                set_current_state(TASK_RUNNING);
                return retval;
	}
}

static int globalfifo_ioctl(struct inode *inodep, struct file *filp, unsigned int cmd, unsigned long arg)
{ 
	struct globalfifo_dev *dev = filp->private_data;
	
	switch (cmd) {
		case GLOBALFIFO_IOCCLEAR:
			if (down_interruptible(&globalfifo_devp->globalfifo_sem)) { //not available return
				return -ERESTARTSYS;
			}
			memset(dev->mem, 0, GLOBALFIFO_SIZE);
			printk(KERN_INFO "globalfifo is set to 0\n");
			up(&globalfifo_devp->globalfifo_sem);//release globalfifo_sem
			break;
		default:
			return -ENOTTY;
	}
	return 0;
}

static loff_t globalfifo_llseek(struct file *filp, loff_t offset, int orig)
{
	loff_t ret;
	ret = 0;
	switch(orig) {
		case 0:
			if (offset < 0) {
				ret = -EINVAL;
				break;
			}
			if ((unsigned int)offset > GLOBALFIFO_SIZE) {
				ret = -EINVAL;
				break;
			}
			filp->f_pos = (unsigned int)offset;
			ret = filp->f_pos;
			break;
		case 1:
			if ((filp->f_pos +offset) > GLOBALFIFO_SIZE) {
				ret = -EINVAL;
                                break;
                        }
			if ((filp->f_pos + offset) < 0) {
				ret = -EINVAL;
                                break;
                        }
			filp->f_pos += offset;
			ret = filp->f_pos;
			break;
		default:
			ret = -EINVAL;
			break;
	}
	return ret;
}

static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
{
	unsigned int mask = 0;
	struct globalfifo_dev *dev = filp->private_data;
	
	if (down_interruptible(&dev->globalfifo_sem)) {
		return -ERESTARTSYS;
	}
	
	poll_wait(filp, &dev->read_wait, wait);
	poll_wait(filp, &dev->write_wait, wait);

	if (dev->current_len != 0) {
		mask |= POLLIN | POLLRDNORM;
	}

	if (dev->current_len != GLOBALFIFO_SIZE) {
		mask |= POLLOUT | POLLWRNORM;
	}
	
	up(&dev->globalfifo_sem);
	return mask;	
}

static const struct file_operations globalfifo_fops = 
{
	.owner = THIS_MODULE,
	.open = globalfifo_open,
	.release = globalfifo_release,
	.read = globalfifo_read,
	.write = globalfifo_write,
	.ioctl = globalfifo_ioctl,
	.llseek = globalfifo_llseek,
	.poll = globalfifo_poll,
};

int globalfifo_init(void) 
{
	int result;
	int error;
	dev_t devno;
	result = alloc_chrdev_region(&devno,0,1,"globalfifo");
	if (result < 0) {
		return result;
	}
	globalfifo_major =MAJOR(devno);
	globalfifo_devp = kmalloc(sizeof(struct globalfifo_dev), GFP_KERNEL);
	if (!globalfifo_devp) {
		result = -ENOMEM;
		goto fail_malloc;
	}
	memset(globalfifo_devp,0,sizeof(struct globalfifo_dev));
	cdev_init(&globalfifo_devp->cdev,&globalfifo_fops);
	globalfifo_devp->cdev.owner = THIS_MODULE;
	globalfifo_devp->cdev.ops = &globalfifo_fops;
	error = cdev_add(&globalfifo_devp->cdev, devno, 1);
	if (error) {
		printk(KERN_NOTICE "Error %d adding globalfifo\n", error);
		goto fail_malloc;
	}

	init_MUTEX(&globalfifo_devp->globalfifo_sem); //semaphore not lock
	//sema_init(&globalfifo_devp->globalfifo_sem,1);
	init_waitqueue_head(&globalfifo_devp->read_wait); 
	init_waitqueue_head(&globalfifo_devp->write_wait); 
	
	globalfifo_class =class_create(THIS_MODULE, "globalfifo");		
	if(IS_ERR(globalfifo_class)) {
        	printk("Error: failed in creating globalfifo class.\n");
		return 0;
	}
	device_create(globalfifo_class,NULL, devno, NULL,"globalfifo");
	return 0;
fail_malloc:
	unregister_chrdev_region(devno,1);
	return result;
}

void globalfifo_exit(void)
{
	cdev_del(&globalfifo_devp->cdev);
	kfree(globalfifo_devp);
	unregister_chrdev_region(MKDEV(globalfifo_major, 0), 1);
	device_destroy(globalfifo_class, MKDEV(globalfifo_major, 0));
	class_destroy(globalfifo_class);
}		
	
MODULE_AUTHOR("wangxy");
MODULE_LICENSE("GPL");

module_init(globalfifo_init);
module_exit(globalfifo_exit);
